/*
 * Copyright 2007 Ivan Dubrov
 * Copyright 2007, 2008, 2009 Robin Helgelin
 * Copyright 2009 Michael Gerzabek
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.dayatang.security.services;

import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;

import org.apache.tapestry5.ioc.Configuration;
import org.apache.tapestry5.ioc.MappedConfiguration;
import org.apache.tapestry5.ioc.OrderedConfiguration;
import org.apache.tapestry5.ioc.ServiceBinder;
import org.apache.tapestry5.ioc.annotations.Contribute;
import org.apache.tapestry5.ioc.annotations.Inject;
import org.apache.tapestry5.ioc.annotations.InjectService;
import org.apache.tapestry5.ioc.annotations.Marker;
import org.apache.tapestry5.ioc.annotations.Value;
import org.apache.tapestry5.ioc.services.ServiceOverride;
import org.apache.tapestry5.services.ComponentClassTransformWorker;
import org.apache.tapestry5.services.HttpServletRequestFilter;
import org.apache.tapestry5.services.LibraryMapping;
import org.apache.tapestry5.services.RequestFilter;
import org.apache.tapestry5.services.RequestGlobals;
import org.springframework.security.access.AccessDecisionManager;
import org.springframework.security.access.AccessDecisionVoter;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.access.vote.AffirmativeBased;
import org.springframework.security.access.vote.RoleVoter;
import org.springframework.security.authentication.AnonymousAuthenticationProvider;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.AuthenticationTrustResolver;
import org.springframework.security.authentication.AuthenticationTrustResolverImpl;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.authentication.RememberMeAuthenticationProvider;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.authentication.encoding.Md5PasswordEncoder;
import org.springframework.security.authentication.encoding.PasswordEncoder;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.memory.UserAttribute;
import org.springframework.security.core.userdetails.memory.UserAttributeEditor;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.access.intercept.DefaultFilterInvocationSecurityMetadataSource;
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
import org.springframework.security.web.access.intercept.RequestKey;
import org.springframework.security.web.authentication.AnonymousAuthenticationFilter;
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
import org.springframework.security.web.authentication.RememberMeServices;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.logout.LogoutHandler;
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
import org.springframework.security.web.authentication.rememberme.RememberMeAuthenticationFilter;
import org.springframework.security.web.authentication.rememberme.TokenBasedRememberMeServices;
import org.springframework.security.web.context.SecurityContextPersistenceFilter;
import org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter;
import org.springframework.security.web.util.AntUrlPathMatcher;

import com.dayatang.security.services.internal.HttpServletRequestFilterWrapper;
import com.dayatang.security.services.internal.LogoutServiceImpl;
import com.dayatang.security.services.internal.RequestFilterWrapper;
import com.dayatang.security.services.internal.SaltSourceImpl;
import com.dayatang.security.services.internal.SecurityChecker;
import com.dayatang.security.services.internal.SpringSecurityExceptionTranslationFilter;
import com.dayatang.security.services.internal.SpringSecurityWorker;
import com.dayatang.security.services.internal.StaticSecurityChecker;
import com.dayatang.security.services.internal.T5AccessDeniedHandler;
import com.dayatang.security.services.internal.TapestryLogoutHandler;

/**
 * This module is automatically included as part of the Tapestry IoC Registry,
 * 
 * @author Ivan Dubrov
 * @author Robin Helgelin
 * @author Michael Gerzabek
 */
public class SecurityModule {

	@SuppressWarnings("unchecked")
	public static void bind(final ServiceBinder binder) {

		binder.bind(LogoutService.class, LogoutServiceImpl.class).withMarker(SpringSecurityServices.class);
		binder.bind(AuthenticationTrustResolver.class, AuthenticationTrustResolverImpl.class).withMarker(
				SpringSecurityServices.class);
		// binder.bind( PasswordEncoder.class, PlaintextPasswordEncoder.class
		// ).withMarker( SpringSecurityServices.class );
		binder.bind(PasswordEncoder.class, Md5PasswordEncoder.class).withMarker(SpringSecurityServices.class);
	}

	@Contribute(ServiceOverride.class)
	public static void setupApplicationServiceOverrides(
			@SpringSecurityServices SaltSourceService saltSource,
			@SpringSecurityServices UsernamePasswordAuthenticationFilter authenticationProcessingFilter,
			MappedConfiguration<Class<?>, Object> configuration) {
		configuration.add(SaltSourceService.class, saltSource);
		configuration.add(UsernamePasswordAuthenticationFilter.class, authenticationProcessingFilter);
	}

	@Marker(SpringSecurityServices.class)
	public static SaltSourceService buildSaltSource(@Inject @Value("${spring-security.password.salt}") final String salt)
			throws Exception {

		SaltSourceImpl saltSource = new SaltSourceImpl();
		saltSource.setSystemWideSalt(salt);
		saltSource.afterPropertiesSet();
		return saltSource;
	}

	public static void contributeFactoryDefaults(final MappedConfiguration<String, String> configuration) {

		configuration.add("spring-security.check.url", "/j_spring_security_check");
		configuration.add("spring-security.failure.url", "/loginfailed");
		configuration.add("spring-security.target.url", "/");
		configuration.add("spring-security.afterlogout.url", "/");
		configuration.add("spring-security.accessDenied.url", "");
		configuration.add("spring-security.force.ssl.login", "false");
		configuration.add("spring-security.rememberme.key", "REMEMBERMEKEY");
		configuration.add("spring-security.loginform.url", "/loginpage");
		configuration.add("spring-security.anonymous.key", "spring_anonymous");
		configuration.add("spring-security.anonymous.attribute", "anonymous,ROLE_ANONYMOUS");
		configuration.add("spring-security.password.salt", "DEADBEEF");
		configuration.add("spring-security.always.use.target.url", "false");
	}

	public static void contributeComponentClassTransformWorker(
			OrderedConfiguration<ComponentClassTransformWorker> configuration, SecurityChecker securityChecker) {

		configuration.add("SpringSecurity", new SpringSecurityWorker(securityChecker), "after:CleanupRender");
	}

	public static void contributeHttpServletRequestHandler(
			OrderedConfiguration<HttpServletRequestFilter> configuration,
			@InjectService("SecurityContextPersistenceFilter") HttpServletRequestFilter httpSessionContextIntegrationFilter,
			@InjectService("AuthenticationProcessingFilter") HttpServletRequestFilter authenticationProcessingFilter,
			@InjectService("RememberMeAuthenticationFilter") HttpServletRequestFilter rememberMeProcessingFilter,
			@InjectService("SecurityContextHolderAwareRequestFilter") HttpServletRequestFilter securityContextHolderAwareRequestFilter,
			@InjectService("AnonymousAuthenticationFilter") HttpServletRequestFilter AnonymousAuthenticationFilter,
			@InjectService("FilterSecurityInterceptor") HttpServletRequestFilter filterSecurityInterceptor,
			@InjectService("SpringSecurityExceptionFilter") SpringSecurityExceptionTranslationFilter springSecurityExceptionFilter) {

		configuration.add("springSecuritySecurityContextPersistenceFilter", httpSessionContextIntegrationFilter,
				"before:springSecurity*");
		configuration.add("springSecurityAuthenticationProcessingFilter", authenticationProcessingFilter);
		configuration.add("springSecurityRememberMeAuthenticationFilter", rememberMeProcessingFilter);
		configuration.add("springSecuritySecurityContextHolderAwareRequestFilter",
				securityContextHolderAwareRequestFilter, "after:springSecurityRememberMeAuthenticationFilter");
		configuration.add("springSecurityAnonymousAuthenticationFilter", AnonymousAuthenticationFilter,
				"after:springSecurityRememberMeAuthenticationFilter",
				"after:springSecurityAuthenticationProcessingFilter");
		configuration.add("springSecurityExceptionFilter", new HttpServletRequestFilterWrapper(
				springSecurityExceptionFilter), "before:springSecurityFilterSecurityInterceptor");
		configuration
				.add("springSecurityFilterSecurityInterceptor", filterSecurityInterceptor, "after:springSecurity*");
	}

	@Marker(SpringSecurityServices.class)
	public static HttpServletRequestFilter buildFilterSecurityInterceptor(
			@SpringSecurityServices final AccessDecisionManager accessDecisionManager,
			@SpringSecurityServices final AuthenticationManager manager,
			final Collection<RequestInvocationDefinition> contributions) throws Exception {

		FilterSecurityInterceptor interceptor = new FilterSecurityInterceptor();
		LinkedHashMap<RequestKey, Collection<ConfigAttribute>> requestMap = convertCollectionToLinkedHashMap(contributions);
		DefaultFilterInvocationSecurityMetadataSource source = new DefaultFilterInvocationSecurityMetadataSource(
				new AntUrlPathMatcher(true), requestMap);
		interceptor.setAccessDecisionManager(accessDecisionManager);
		interceptor.setAlwaysReauthenticate(false);
		interceptor.setAuthenticationManager(manager);
		interceptor.setSecurityMetadataSource(source);
		interceptor.setValidateConfigAttributes(true);
		interceptor.afterPropertiesSet();
		return new HttpServletRequestFilterWrapper(interceptor);
	}

	static LinkedHashMap<RequestKey, Collection<ConfigAttribute>> convertCollectionToLinkedHashMap(
			Collection<RequestInvocationDefinition> urls) {

		LinkedHashMap<RequestKey, Collection<ConfigAttribute>> requestMap = new LinkedHashMap<RequestKey, Collection<ConfigAttribute>>();
		for (RequestInvocationDefinition url : urls) {

			requestMap.put(url.getRequestKey(), url.getConfigAttributeDefinition());
		}
		return requestMap;
	}

	@Marker(SpringSecurityServices.class)
	public static HttpServletRequestFilter buildSecurityContextPersistenceFilter() throws Exception {

		SecurityContextPersistenceFilter filter = new SecurityContextPersistenceFilter();
		filter.setForceEagerSessionCreation(false);
		filter.afterPropertiesSet();
		return new HttpServletRequestFilterWrapper(filter);
	}

	@Marker(SpringSecurityServices.class)
	public static UsernamePasswordAuthenticationFilter buildRealAuthenticationProcessingFilter(
			@SpringSecurityServices final AuthenticationManager manager,
			@SpringSecurityServices final RememberMeServices rememberMeServices,
			@Inject @Value("${spring-security.check.url}") final String authUrl,
			@Inject @Value("${spring-security.target.url}") final String targetUrl,
			@Inject @Value("${spring-security.failure.url}") final String failureUrl,
			@Inject @Value("${spring-security.always.use.target.url}") final String alwaysUseTargetUrl)
			throws Exception {

		UsernamePasswordAuthenticationFilter filter = new UsernamePasswordAuthenticationFilter();
		filter.setAuthenticationManager(manager);

		filter.setAuthenticationFailureHandler(new SimpleUrlAuthenticationFailureHandler(failureUrl));

		SavedRequestAwareAuthenticationSuccessHandler successHandler = new SavedRequestAwareAuthenticationSuccessHandler();
		successHandler.setDefaultTargetUrl(targetUrl);
		successHandler.setAlwaysUseDefaultTargetUrl(Boolean.parseBoolean(alwaysUseTargetUrl));
		filter.setAuthenticationSuccessHandler(successHandler);
		filter.setFilterProcessesUrl(targetUrl);
		filter.setFilterProcessesUrl(authUrl);
		filter.setRememberMeServices(rememberMeServices);

		filter.afterPropertiesSet();
		return filter;
	}

	@Marker(SpringSecurityServices.class)
	public static HttpServletRequestFilter buildAuthenticationProcessingFilter(
			final UsernamePasswordAuthenticationFilter filter) throws Exception {

		return new HttpServletRequestFilterWrapper(filter);
	}

	@Marker(SpringSecurityServices.class)
	public static HttpServletRequestFilter buildRememberMeAuthenticationFilter(
			@SpringSecurityServices final RememberMeServices rememberMe,
			@SpringSecurityServices final AuthenticationManager authManager) throws Exception {

		RememberMeAuthenticationFilter filter = new RememberMeAuthenticationFilter();
		filter.setRememberMeServices(rememberMe);
		filter.setAuthenticationManager(authManager);
		filter.afterPropertiesSet();
		return new HttpServletRequestFilterWrapper(filter);
	}

	@Marker(SpringSecurityServices.class)
	public static HttpServletRequestFilter buildSecurityContextHolderAwareRequestFilter() {

		return new HttpServletRequestFilterWrapper(new SecurityContextHolderAwareRequestFilter());
	}

	@Marker(SpringSecurityServices.class)
	public static HttpServletRequestFilter buildAnonymousAuthenticationFilter(
			@Inject @Value("${spring-security.anonymous.attribute}") final String anonymousAttr,
			@Inject @Value("${spring-security.anonymous.key}") final String anonymousKey) throws Exception {

		AnonymousAuthenticationFilter filter = new AnonymousAuthenticationFilter();
		filter.setKey(anonymousKey);
		UserAttributeEditor attrEditor = new UserAttributeEditor();
		attrEditor.setAsText(anonymousAttr);
		UserAttribute attr = (UserAttribute) attrEditor.getValue();
		filter.setUserAttribute(attr);
		filter.afterPropertiesSet();
		return new HttpServletRequestFilterWrapper(filter);
	}

	@Marker(SpringSecurityServices.class)
	public static RememberMeServices build(final UserDetailsService userDetailsService,
			@Inject @Value("${spring-security.rememberme.key}") final String rememberMeKey) {

		TokenBasedRememberMeServices rememberMe = new TokenBasedRememberMeServices();
		rememberMe.setUserDetailsService(userDetailsService);
		rememberMe.setKey(rememberMeKey);
		return rememberMe;
	}

	@Marker(SpringSecurityServices.class)
	public static LogoutHandler buildRememberMeLogoutHandler(final UserDetailsService userDetailsService,
			@Inject @Value("${spring-security.rememberme.key}") final String rememberMeKey) throws Exception {

		TokenBasedRememberMeServices rememberMe = new TokenBasedRememberMeServices();
		rememberMe.setUserDetailsService(userDetailsService);
		rememberMe.setKey(rememberMeKey);
		rememberMe.afterPropertiesSet();
		return rememberMe;
	}

	public static void contributeLogoutService(final OrderedConfiguration<LogoutHandler> cfg,
			@Inject RequestGlobals globals,
			@InjectService("RememberMeLogoutHandler") final LogoutHandler rememberMeLogoutHandler) {

		cfg.add("securityContextLogoutHandler", new SecurityContextLogoutHandler());
		cfg.add("rememberMeLogoutHandler", rememberMeLogoutHandler);
		cfg.add("tapestryLogoutHandler", new TapestryLogoutHandler(globals), new String[0]);
	}

	@Marker(SpringSecurityServices.class)
	public static AuthenticationManager buildProviderManager(final List<AuthenticationProvider> providers)
			throws Exception {

		ProviderManager manager = new ProviderManager();
		manager.setProviders(providers);
		manager.afterPropertiesSet();
		return manager;
	}

	@Marker(SpringSecurityServices.class)
	public final AuthenticationProvider buildAnonymousAuthenticationProvider(
			@Inject @Value("${spring-security.anonymous.key}") final String anonymousKey) throws Exception {

		AnonymousAuthenticationProvider provider = new AnonymousAuthenticationProvider();
		provider.setKey(anonymousKey);
		provider.afterPropertiesSet();
		return provider;
	}

	@Marker(SpringSecurityServices.class)
	public final AuthenticationProvider buildRememberMeAuthenticationProvider(
			@Inject @Value("${spring-security.rememberme.key}") final String rememberMeKey) throws Exception {

		RememberMeAuthenticationProvider provider = new RememberMeAuthenticationProvider();
		provider.setKey(rememberMeKey);
		provider.afterPropertiesSet();
		return provider;
	}

	@Marker(SpringSecurityServices.class)
	public final AuthenticationProvider buildDaoAuthenticationProvider(final UserDetailsService userDetailsService,
			final PasswordEncoder passwordEncoder, final SaltSourceService saltSource) throws Exception {

		DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
		provider.setUserDetailsService(userDetailsService);
		provider.setPasswordEncoder(passwordEncoder);
		provider.setSaltSource(saltSource);
		provider.afterPropertiesSet();
		return provider;
	}

	public final void contributeProviderManager(
			final OrderedConfiguration<AuthenticationProvider> configuration,
			@InjectService("AnonymousAuthenticationProvider") final AuthenticationProvider anonymousAuthenticationProvider,
			@InjectService("RememberMeAuthenticationProvider") final AuthenticationProvider rememberMeAuthenticationProvider) {

		configuration.add("anonymousAuthenticationProvider", anonymousAuthenticationProvider);
		configuration.add("rememberMeAuthenticationProvider", rememberMeAuthenticationProvider);
	}

	@Marker(SpringSecurityServices.class)
	public final AccessDecisionManager buildAccessDecisionManager(final List<AccessDecisionVoter> voters)
			throws Exception {

		AffirmativeBased manager = new AffirmativeBased();
		manager.setDecisionVoters(voters);
		manager.afterPropertiesSet();
		return manager;
	}

	public final void contributeAccessDecisionManager(final OrderedConfiguration<AccessDecisionVoter> configuration) {

		configuration.add("RoleVoter", new RoleVoter());
	}

	@Marker(SpringSecurityServices.class)
	public static SecurityChecker buildSecurityChecker(
			@SpringSecurityServices final AccessDecisionManager accessDecisionManager,
			@SpringSecurityServices final AuthenticationManager authenticationManager) throws Exception {

		StaticSecurityChecker checker = new StaticSecurityChecker();

		checker.setAccessDecisionManager(accessDecisionManager);
		checker.setAuthenticationManager(authenticationManager);
		checker.afterPropertiesSet();
		return checker;
	}

	@Marker(SpringSecurityServices.class)
	public static AuthenticationEntryPoint buildAuthenticationEntryPoint(
			@Inject @Value("${spring-security.loginform.url}") final String loginFormUrl,
			@Inject @Value("${spring-security.force.ssl.login}") final String forceHttps) throws Exception {

		LoginUrlAuthenticationEntryPoint entryPoint = new LoginUrlAuthenticationEntryPoint();
		entryPoint.setLoginFormUrl(loginFormUrl);
		entryPoint.afterPropertiesSet();
		boolean forceSSL = Boolean.parseBoolean(forceHttps);
		entryPoint.setForceHttps(forceSSL);
		return entryPoint;
	}

	public static SpringSecurityExceptionTranslationFilter buildSpringSecurityExceptionFilter(
			final AuthenticationEntryPoint aep,
			@Inject @Value("${spring-security.accessDenied.url}") final String accessDeniedUrl) throws Exception {

		SpringSecurityExceptionTranslationFilter filter = new SpringSecurityExceptionTranslationFilter();
		filter.setAuthenticationEntryPoint(aep);
		if (!accessDeniedUrl.equals("")) {
			T5AccessDeniedHandler accessDeniedHandler = new T5AccessDeniedHandler();
			accessDeniedHandler.setErrorPage(accessDeniedUrl);
			filter.setAccessDeniedHandler(accessDeniedHandler);
		}
		filter.afterPropertiesSet();
		return filter;
	}

	public static void contributeRequestHandler(
			final OrderedConfiguration<RequestFilter> configuration,
			final RequestGlobals globals,
			@InjectService("SpringSecurityExceptionFilter") final SpringSecurityExceptionTranslationFilter springSecurityExceptionFilter) {

		configuration.add("SpringSecurityExceptionFilter", new RequestFilterWrapper(globals,
				springSecurityExceptionFilter), "after:ErrorFilter");
	}

	// Contribute three aspects of module: presentation, entities and
	// configuration
	public static void contributeComponentClassResolver(final Configuration<LibraryMapping> configuration) {

		configuration.add(new LibraryMapping("security", "com.dayatang.security"));
	}

}
